/***************************************
 * 
 * Android Bluetooth Oscilloscope
 * yus	-	projectproto.blogspot.com
 * September 2010
 *  
 ***************************************/

package org.projectproto.yuscope;

import android.app.Activity;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.os.PowerManager;
import android.view.View;
import android.view.Window;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.RadioButton;
import android.widget.TextView;
import android.widget.Toast;
import android.widget.ToggleButton;

public class BluetoothOscilloscope extends Activity implements  Button.OnClickListener{
	
	// Message types sent from the BluetoothRfcommClient Handler
    public static final int MESSAGE_STATE_CHANGE = 1;
    public static final int MESSAGE_READ = 2;
    public static final int MESSAGE_WRITE = 3;
    public static final int MESSAGE_DEVICE_NAME = 4;
    public static final int MESSAGE_TOAST = 5;
    
	// Key names received from the BluetoothRfcommClient Handler
    public static final String DEVICE_NAME = "device_name";
    public static final String TOAST = "toast";
    
    // Intent request codes
    private static final int REQUEST_CONNECT_DEVICE = 1;
    private static final int REQUEST_ENABLE_BT = 2;
    
	// bt-uart constants
    private static final int MAX_SAMPLES = 640;
    private static final int  MAX_LEVEL	= 240;
    private static final int  DATA_START = (MAX_LEVEL + 1);
    private static final int  DATA_END = (MAX_LEVEL + 2);
    
    private static final byte  REQ_DATA = 0x00;
    private static final byte  ADJ_HORIZONTAL = 0x01;
    private static final byte  ADJ_VERTICAL = 0x02;
    private static final byte  ADJ_POSITION = 0x03;

    private static final byte  CHANNEL1 = 0x01;
    private static final byte  CHANNEL2 = 0x02;
    
    // Run/Pause status
    private boolean bReady = false;
    // receive data 
    private int[] ch1_data = new int[MAX_SAMPLES/2];
	private int[] ch2_data = new int[MAX_SAMPLES/2];
    private int dataIndex=0, dataIndex1=0, dataIndex2=0;
	private boolean bDataAvailable=false;
    
    // Layout Views
    private TextView mBTStatus;
    private Button mConnectButton;
    private RadioButton rb1, rb2;
    private TextView ch1pos_label, ch2pos_label;
    private Button btn_pos_up, btn_pos_down;
    private TextView ch1_scale, ch2_scale;
    private Button btn_scale_up, btn_scale_down;
    private TextView time_per_div;
    private Button timebase_inc, timebase_dec;
    private ToggleButton run_buton;
    
    public WaveformView mWaveform = null;
    
    // Name of the connected device
    private String mConnectedDeviceName = null;
    // Local Bluetooth adapter
    private BluetoothAdapter mBluetoothAdapter = null;
    // Member object for the RFCOMM services
    private BluetoothRfcommClient mRfcommClient = null;
    
    static String[] timebase = {"5us", "10us", "20us", "50us", "100us", "200us", "500us", "1ms", "2ms", "5ms", "10ms", "20ms", "50ms" };
    static String[] ampscale = {"10mV", "20mV", "50mV", "100mV", "200mV", "500mV", "1V", "2V", "GND"};
    static byte timebase_index = 5;
    static byte ch1_index = 4, ch2_index = 5;
    static byte ch1_pos = 24, ch2_pos = 17;	// 0 to 40
    
    // stay awake
    protected PowerManager.WakeLock mWakeLock;
    
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        
        // Set up the window layout
        requestWindowFeature(Window.FEATURE_NO_TITLE);        
        setContentView(R.layout.main);
        
        // Get local Bluetooth adapter
        mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
        
        // If the adapter is null, then Bluetooth is not supported
        if (mBluetoothAdapter == null) {
            Toast.makeText(this, "Bluetooth is not available", Toast.LENGTH_LONG).show();
            finish();
            return;
        }
        
        // Prevent phone from sleeping
        PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
        this.mWakeLock = pm.newWakeLock(PowerManager.SCREEN_BRIGHT_WAKE_LOCK, "My Tag"); 
        this.mWakeLock.acquire();
    }
    
    @Override
    public void onStart(){
    	super.onStart();
    	
    	// If BT is not on, request that it be enabled.
    	if (!mBluetoothAdapter.isEnabled()){
    		Intent enableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
    		startActivityForResult(enableIntent, REQUEST_ENABLE_BT);
    	}
    	// Otherwise, setup the Oscillosope session
    	else{
    		if (mRfcommClient == null) setupOscilloscope();
    	}    	
    }
    
    @Override
    public synchronized void onResume(){
    	super.onResume();
    	// Performing this check in onResume() covers the case in which BT was
        // not enabled during onStart(), so we were paused to enable it...
        // onResume() will be called when ACTION_REQUEST_ENABLE activity returns.
        if (mRfcommClient != null) {
            // Only if the state is STATE_NONE, do we know that we haven't started already
            if (mRfcommClient.getState() == BluetoothRfcommClient.STATE_NONE) {
              // Start the Bluetooth  RFCOMM services
              mRfcommClient.start();
            }
        }
    }
    
    @Override
    public void  onClick(View v){
    	int buttonID;
    	buttonID = v.getId();
    	switch (buttonID){
    	case R.id.btn_position_up :
    		if(rb1.isChecked() && (ch1_pos<38) ){
    			ch1_pos += 1; ch1pos_label.setPadding(0, toScreenPos(ch1_pos), 0, 0);
    			sendMessage( new String(new byte[] {ADJ_POSITION, CHANNEL1, ch1_pos}) );
    		}
    		else if(rb2.isChecked() && (ch2_pos<38) ){
    			ch2_pos += 1; ch2pos_label.setPadding(0, toScreenPos(ch2_pos), 0, 0);
    			sendMessage( new String(new byte[] {ADJ_POSITION, CHANNEL2, ch2_pos}) );
    		}
    		break;
    	case R.id.btn_position_down :
    		if(rb1.isChecked() && (ch1_pos>4) ){
    			ch1_pos -= 1; ch1pos_label.setPadding(0, toScreenPos(ch1_pos), 0, 0);
    			sendMessage( new String(new byte[] {ADJ_POSITION, CHANNEL1, ch1_pos}) );
    		}
    		else if(rb2.isChecked() && (ch2_pos>4) ){
    			ch2_pos -= 1; ch2pos_label.setPadding(0, toScreenPos(ch2_pos), 0, 0);
    			sendMessage( new String(new byte[] {ADJ_POSITION, CHANNEL2, ch2_pos}) );
    		}
    		break;
    	case R.id.btn_scale_increase :
    		if(rb1.isChecked() && (ch1_index>0)){
    			ch1_scale.setText(ampscale[--ch1_index]);
    			sendMessage( new String(new byte[] {ADJ_VERTICAL, CHANNEL1, ch1_index}) );
    		}
    		else if(rb2.isChecked() && (ch2_index>0)){
    			ch2_scale.setText(ampscale[--ch2_index]);
    			sendMessage( new String(new byte[] {ADJ_VERTICAL, CHANNEL2, ch2_index}) );
    		}
    		break;
    	case R.id.btn_scale_decrease :
    		if(rb1.isChecked() && (ch1_index<(ampscale.length-1))){
    			ch1_scale.setText(ampscale[++ch1_index]);
    			sendMessage( new String(new byte[] {ADJ_VERTICAL, CHANNEL1, ch1_index}) );
    		}
    		else if(rb2.isChecked() && (ch2_index<(ampscale.length-1))){
    			ch2_scale.setText(ampscale[++ch2_index]);
    			sendMessage( new String(new byte[] {ADJ_VERTICAL, CHANNEL2, ch2_index}) );
    		}
    		break;
    	case R.id.btn_timebase_increase :
    		if(timebase_index<(timebase.length-1)){
    			time_per_div.setText(timebase[++timebase_index]);
    			sendMessage( new String(new byte[] {ADJ_HORIZONTAL, timebase_index}) );
    		}
    		break;
    	case R.id.btn_timebase_decrease :
    		if(timebase_index>0){
    			time_per_div.setText(timebase[--timebase_index]);
    			sendMessage( new String(new byte[] {ADJ_HORIZONTAL, timebase_index}) );
    		}
    		break;
    	case R.id.tbtn_runtoggle :
    		if(run_buton.isChecked()){
    			sendMessage( new String(new byte[] {
    					ADJ_HORIZONTAL, timebase_index,
    					ADJ_VERTICAL, CHANNEL1, ch1_index,
    					ADJ_VERTICAL, CHANNEL2, ch2_index,
    					ADJ_POSITION, CHANNEL1, ch1_pos,
    					ADJ_POSITION, CHANNEL2, ch2_pos,
    					REQ_DATA}) );
    			bReady = true;
    		}else{
    			bReady = false;
    		}
    		break;
    	}
    }
    
    @Override
    public void onDestroy(){
    	super.onDestroy();
    	// Stop the Bluetooth RFCOMM services
        if (mRfcommClient != null) mRfcommClient.stop();
        // release screen being on
        if (mWakeLock.isHeld()) { 
            mWakeLock.release();
        }
    }
        
    /**
     * Sends a message.
     * @param message  A string of text to send.
     */
    private void sendMessage(String message){
    	// Check that we're actually connected before trying anything
    	if (mRfcommClient.getState() != BluetoothRfcommClient.STATE_CONNECTED) {
    		Toast.makeText(this, R.string.not_connected, Toast.LENGTH_SHORT).show();
    		return;
    	}
    	// Check that there's actually something to send
    	if (message.length() > 0) {
    		// Get the message bytes and tell the BluetoothRfcommClient to write
    		byte[] send = message.getBytes();
    		mRfcommClient.write(send);
    	}
    }
    
    private void setupOscilloscope(){
    	
    	mBTStatus = (TextView) findViewById(R.id.txt_btstatus);
    	
    	mConnectButton = (Button) findViewById(R.id.button_connect);
    	mConnectButton.setOnClickListener(new OnClickListener(){
			public void onClick(View arg0) {
				BTConnect();
			}    		
    	});
    	
    	rb1 = (RadioButton)findViewById(R.id.rbtn_ch1);
        rb2 = (RadioButton)findViewById(R.id.rbtn_ch2);

        ch1pos_label = (TextView) findViewById(R.id.txt_ch1pos);
        ch2pos_label = (TextView) findViewById(R.id.txt_ch2pos);
        ch1pos_label.setPadding(0, toScreenPos(ch1_pos), 0, 0);
        ch2pos_label.setPadding(0, toScreenPos(ch2_pos), 0, 0);
        
        btn_pos_up = (Button) findViewById(R.id.btn_position_up);
        btn_pos_down = (Button) findViewById(R.id.btn_position_down);
        btn_pos_up.setOnClickListener(this);
        btn_pos_down.setOnClickListener(this);
        
        ch1_scale = (TextView) findViewById(R.id.txt_ch1_scale);
        ch2_scale = (TextView) findViewById(R.id.txt_ch2_scale);
        ch1_scale.setText(ampscale[ch1_index]);
        ch2_scale.setText(ampscale[ch2_index]);
        
        btn_scale_up = (Button) findViewById(R.id.btn_scale_increase);
        btn_scale_down = (Button) findViewById(R.id.btn_scale_decrease);
        btn_scale_up.setOnClickListener(this);
        btn_scale_down.setOnClickListener(this);
        
        time_per_div = (TextView)findViewById(R.id.txt_timebase);
        time_per_div.setText(timebase[timebase_index]);
        timebase_inc = (Button) findViewById(R.id.btn_timebase_increase);
        timebase_dec = (Button) findViewById(R.id.btn_timebase_decrease);
        timebase_inc.setOnClickListener(this);
        timebase_dec.setOnClickListener(this);
        
        run_buton = (ToggleButton) findViewById(R.id.tbtn_runtoggle);
        run_buton.setOnClickListener(this);
        
    	// Initialize the BluetoothRfcommClient to perform bluetooth connections
        mRfcommClient = new BluetoothRfcommClient(this, mHandler);
        
        // waveform / plot area
        mWaveform = (WaveformView)findViewById(R.id.WaveformArea);
    }
    
    private void BTConnect(){
    	Intent serverIntent = new Intent(this, DeviceListActivity.class);
    	startActivityForResult(serverIntent, REQUEST_CONNECT_DEVICE);
    }
    
    private int toScreenPos(byte position){
    	//return ( (int)MAX_LEVEL - (int)position*6 );
    	return ( (int)MAX_LEVEL - (int)position*6 - 7);
    }
    
    // The Handler that gets information back from the BluetoothRfcommClient
    private final Handler mHandler = new Handler(){
    	@Override
        public void handleMessage(Message msg){
    		switch (msg.what){
    		case MESSAGE_STATE_CHANGE:
    			switch (msg.arg1){
    			case BluetoothRfcommClient.STATE_CONNECTED:
    				mBTStatus.setText(R.string.title_connected_to);
                    mBTStatus.append("\n" + mConnectedDeviceName);
    				break;
    			case BluetoothRfcommClient.STATE_CONNECTING:
    				mBTStatus.setText(R.string.title_connecting);
    				break;
    			case BluetoothRfcommClient.STATE_NONE:
    				mBTStatus.setText(R.string.title_not_connected);
    				break;
    			}
    			break;
    		case MESSAGE_READ: // todo: implement receive data buffering
    			byte[] readBuf = (byte[]) msg.obj;
    			int data_length = msg.arg1;
    			for(int x=0; x<data_length; x++){
    				int raw = UByte(readBuf[x]);
    				if( raw>MAX_LEVEL ){
    					if( raw==DATA_START ){
    						bDataAvailable = true;
    						dataIndex = 0; dataIndex1=0; dataIndex2=0;
    					}
    					else if( (raw==DATA_END) || (dataIndex>=MAX_SAMPLES) ){
    						bDataAvailable = false;
                    		dataIndex = 0; dataIndex1=0; dataIndex2=0;
                    		mWaveform.set_data(ch1_data, ch2_data);
                    		if(bReady){ // send "REQ_DATA" again
                        		BluetoothOscilloscope.this.sendMessage( new String(new byte[] {REQ_DATA}) );
                        	}
                    		//break;
    					}
    				}
    				else if( (bDataAvailable) && (dataIndex<(MAX_SAMPLES)) ){ // valid data
    					if((dataIndex++)%2==0) ch1_data[dataIndex1++] = raw;	// even data
    					else ch2_data[dataIndex2++] = raw;	// odd data
    				}
    			}
    			break;
    		case MESSAGE_DEVICE_NAME:
    			// save the connected device's name
                mConnectedDeviceName = msg.getData().getString(DEVICE_NAME);
                Toast.makeText(getApplicationContext(), "Connected to "
                        + mConnectedDeviceName, Toast.LENGTH_SHORT).show();
    			break;
    		case MESSAGE_TOAST:
    			Toast.makeText(getApplicationContext(), msg.getData().getString(TOAST),
                        Toast.LENGTH_SHORT).show();
    			break;
    		}
    	}
    	// signed to unsigned
    	private int UByte(byte b){
        	if(b<0) // if negative
        		return (int)( (b&0x7F) + 128 );
        	else
        		return (int)b;
        }
    };
    
    public void onActivityResult(int requestCode, int resultCode, Intent data){
    	switch (requestCode) {
    	case REQUEST_CONNECT_DEVICE:
    		// When DeviceListActivity returns with a device to connect
    		if (resultCode == Activity.RESULT_OK){
    			// Get the device MAC address
    			String address = data.getExtras().getString(DeviceListActivity.EXTRA_DEVICE_ADDRESS);
    			// Get the BLuetoothDevice object
                BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(address);
                // Attempt to connect to the device
                mRfcommClient.connect(device);
    		}
    		break;
    	case REQUEST_ENABLE_BT:
    		// When the request to enable Bluetooth returns
            if (resultCode == Activity.RESULT_OK){
            	// Bluetooth is now enabled, so set up the oscilloscope
            	setupOscilloscope();
            }else{
            	// User did not enable Bluetooth or an error occured
                Toast.makeText(this, R.string.bt_not_enabled_leaving, Toast.LENGTH_SHORT).show();
                finish();
            }
    		break;
    	}
    }
}
